package com.five.bi.utils;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.stat.DruidDataSourceStatManager;
import com.alibaba.druid.util.StringUtils;
import com.five.bi.config.DataSourceConfig;
import com.five.bi.model.DatasourceInfo;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import java.lang.reflect.Field;
import java.sql.DriverManager;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
 * @Author: Mr.Lee
 * @Date: 2022/8/13 11:11
 * @param
 * @Description:
 * 切换数据源的核心配置类
 */

//涉及到数据源一定要加事务管理注解
@EnableTransactionManagement
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Autowired
    private DataSourceConfig dataSourceConfig;

//    @Autowired
//    private DatasourceInfoService dataSourceVoService;

    // 通过ThreadLocal线程隔离的优势线程存储线程，当前线程只能操作当前线程的局部变量
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
    // 把已有的数据源封装在Map里
    private Map<Object, Object> dynamicTargetDataSources = new HashMap<>();

    //通过重写AbstractRoutingDataSource的内置函数，来通过当前连接的数据源的key，进行数据源的获取
    @Override
    protected Object determineCurrentLookupKey() {
        if (StringUtils.isEmpty(getDataSource())) {
            return "default";
        }
        return getDataSource();
    }

    // 设置默认数据源（必须要有，否则无法启动）
    @Override
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
    }

    // 通过设置数据源
    @Override
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
    }

    // 切换数据源,更改ThreadLocal中的局部变量
    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    // 获取数据源
    public static String getDataSource() {
        return contextHolder.get();
    }

    // 删除数据源变量，只对能访问到这个线程的对象起作用
    public static void clearDataSource() {
        contextHolder.remove();
    }

    public boolean createDataSource(DatasourceInfo datasourceInfo) throws NoSuchFieldException, IllegalAccessException {
        try {
            // 获取配置在Yaml文件中的所有数据源信息
            Map<String, Object> dataBaseConfig = dataSourceConfig.getDataBaseConfig();
            // 根据数据库类型获取数据源信息
            Map<String, String> dataConfig = (Map<String, String>) dataBaseConfig.get(datasourceInfo.getDataType());
            if (dataConfig == null) {
                // 不支持此类数据源
                throw new RuntimeException("不支持此数据源！");
            }
            String driveName = dataConfig.get("driverClassName");
            // url  替换其中的占位符
            String url = dataConfig.get("url").replaceAll("\\{IP}", datasourceInfo.getUrl())
                    .replaceAll("\\{port}", datasourceInfo.getPort())
                    .replaceAll("\\{database}", datasourceInfo.getDataName());
            // 测试连接
            testConnection(driveName, url, datasourceInfo.getUsername(), datasourceInfo.getPassword());

            // 通过Druid数据库连接池连接数据库
            DruidDataSource dataSource = new DruidDataSource();
            //接收前端传递的参数并且注入进去
            dataSource.setName(datasourceInfo.getDataName());
            dataSource.setUrl(url);
            dataSource.setUsername(datasourceInfo.getUsername());
            dataSource.setPassword(datasourceInfo.getPassword());
            dataSource.setDriverClassName(driveName);
            dataSource.setInitialSize(50); //初始化时建立物理连接的个数。初始化发生在显示调用init方法，或者第一次getConnection时
            dataSource.setMaxActive(200); //最大连接池数量
            dataSource.setMaxWait(60000); //获取连接时最大等待时间，单位毫秒。当链接数已经达到了最大链接数的时候，应用如果还要获取链接就会出现等待的现象，等待链接释放并回到链接池，如果等待的时间过长就应该踢掉这个等待，不然应用很可能出现雪崩现象
            dataSource.setMinIdle(40); //最小连接池数量
            dataSource.setTestWhileIdle(true);//建议配置为true，不影响性能，并且保证安全性。申请连接的时候检测，如果空闲时间大于timeBetweenEvictionRunsMillis，执行validationQuery检测连接是否有效。
            dataSource.setValidationQuery("SELECT 1");
            dataSource.setFilters("stat");//属性类型是字符串，通过别名的方式配置扩展插件，常用的插件有：监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
            dataSource.setTimeBetweenEvictionRunsMillis(60000); //配置间隔多久才进行一次检测，检测需要关闭的空闲连接，单位是毫秒
            dataSource.setMinEvictableIdleTimeMillis(180000); //配置一个连接在池中最小生存的时间，单位是毫秒，这里配置为3分钟180000
            dataSource.setKeepAlive(true); //打开druid.keepAlive之后，当连接池空闲时，池中的minIdle数量以内的连接，空闲时间超过minEvictableIdleTimeMillis，则会执行keepAlive操作，即执行druid.validationQuery指定的查询SQL，一般为select * from dual，只要minEvictableIdleTimeMillis设置的小于防火墙切断连接时间，就可以保证当连接空闲时自动做保活检测，不会被防火墙切断
            dataSource.setRemoveAbandoned(true); //是否移除泄露的连接/超过时间限制是否回收。
            dataSource.setRemoveAbandonedTimeout(3600); //泄露连接的定义时间(要超过最大事务的处理时间)；单位为秒。这里配置为1小时
            dataSource.setLogAbandoned(true);//移除泄露连接发生是是否记录日志
            dataSource.setBreakAfterAcquireFailure(true);
            dataSource.setConnectionErrorRetryAttempts(3);
            dataSource.init();// 数据源初始化
            //获取当前数据源的键值对存入Map
            this.dynamicTargetDataSources.put(datasourceInfo.getKey(), dataSource);
            // 设置数据源
            this.setTargetDataSources(this.dynamicTargetDataSources);
            // 解析数据源
            super.afterPropertiesSet();
            // 切换数据源
            setDataSource(datasourceInfo.getKey());
            /**
             ** 修改mybatis的数据源
             * ！！！重要，不修改mybatis的数据源的话，
             * 即使切换了数据源之后还是会出现默认数据源的情况
             */
            SqlSessionFactory SqlSessionFactory = (SqlSessionFactory) SpringContextUtils.getBean(SqlSessionFactory.class);
            Environment environment = SqlSessionFactory.getConfiguration().getEnvironment();
            Field dataSourceField = environment.getClass().getDeclaredField("dataSource");
            //跳过检验
            dataSourceField.setAccessible(true);
            //修改mybatis的数据源
            dataSourceField.set(environment, dataSource);
            //修改完成后所有线程使用此数据源
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }
    // 删除数据源(每次切换数据源的时候都应先移除已有数据源)
    public boolean delDatasources(String datasourceid) {
        Map<Object, Object> dynamicTargetDataSources2 = this.dynamicTargetDataSources;
        if (dynamicTargetDataSources2.containsKey(datasourceid)) {
            Set<DruidDataSource> druidDataSourceInstances = DruidDataSourceStatManager.getDruidDataSourceInstances();
            for (DruidDataSource l : druidDataSourceInstances) {
                if (datasourceid.equals(l.getName())) {
                    dynamicTargetDataSources2.remove(datasourceid);
                    DruidDataSourceStatManager.removeDataSource(l);
                    setTargetDataSources(dynamicTargetDataSources2);// 将map赋值给父类的TargetDataSources
                    super.afterPropertiesSet();// 将TargetDataSources中的连接信息放入resolvedDataSources管理
                    return true;
                }
            }
            return false;
        } else {
            return false;
        }
    }

    // 测试数据源连接的方法
    public void testConnection(String driveClass, String url, String username, String password) {
        try {
            Class.forName(driveClass);
            DriverManager.getConnection(url, username, password);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }
}

